Skip to content

Conversation

@sonsation
Copy link

This change refactors the PdfDownloader to use Kotlin coroutines and OkHttp's asynchronous enqueue method instead of the synchronous execute method. This makes the download process non-blocking and allows for better cancellation handling.

The makeNetworkRequest function in PdfDownloader is now a suspend function and uses a new awaitCall extension function for OkHttpClient to bridge the callback-based API with coroutines.

The FileUtils.writeFile function is also updated to be a suspend function and uses coroutineScope and ensureActive for better coroutine integration and cancellation.

A new initWithUrl overload is added to PdfRendererView that accepts a LifecycleOwner. This allows the PDF download lifecycle to be tied to the provided LifecycleOwner, ensuring that downloads are properly cancelled when the lifecycle is destroyed.

This change refactors the `PdfDownloader` to use Kotlin coroutines and OkHttp's asynchronous `enqueue` method instead of the synchronous `execute` method. This makes the download process non-blocking and allows for better cancellation handling.

The `makeNetworkRequest` function in `PdfDownloader` is now a suspend function and uses a new `awaitCall` extension function for `OkHttpClient` to bridge the callback-based API with coroutines.

The `FileUtils.writeFile` function is also updated to be a suspend function and uses `coroutineScope` and `ensureActive` for better coroutine integration and cancellation.

A new `initWithUrl` overload is added to `PdfRendererView` that accepts a `LifecycleOwner`. This allows the PDF download lifecycle to be tied to the provided `LifecycleOwner`, ensuring that downloads are properly cancelled when the lifecycle is destroyed.
This commit updates the following:
- Gradle wrapper from 8.11.1 to 8.14.3
- Android Gradle Plugin from 8.10.0 to 8.12.1
- Kotlin version from 2.1.20 to 2.2.10
- Dokka plugin version from 1.9.20 to 2.0.0
- Compile SDK from 35 to 36
- JVM toolchain from 17 to 21
- Compose BOM from 2025.04.01 to 2025.08.00 (and 2025.03.00 to 2025.08.00 in app module)
- Various library dependencies to their latest versions.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the PdfDownloader to use Kotlin coroutines and OkHttp's asynchronous API instead of synchronous operations, improving non-blocking behavior and cancellation handling.

  • Converts makeNetworkRequest and FileUtils.writeFile to suspend functions with proper coroutine integration
  • Adds a new initWithUrl overload that accepts a LifecycleOwner for automatic lifecycle-based cancellation
  • Updates dependencies to newer versions including OkHttp 5.1.0, Gradle, and various Android libraries

Reviewed Changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
PdfDownloader.kt Refactors network requests to use coroutines with async OkHttp API and proper cancellation handling
FileUtils.kt Converts file writing to suspend function with coroutine scope and cancellation checks
PdfRendererView.kt Adds lifecycle-aware PDF initialization method that binds download operations to LifecycleOwner
pdfViewer/build.gradle.kts Updates dependencies including OkHttp, Kotlin, Android libraries and build tools
app/build.gradle.kts Updates app-level dependencies to match library changes
gradle-wrapper.properties Updates Gradle wrapper to version 8.14.3

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +163 to +201
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
val lifecycleScope = lifecycleOwner.lifecycleScope
this@PdfRendererView.cacheStrategy = cacheStrategy
PdfDownloader(
lifecycleScope,
headers,
url,
cacheStrategy,
PdfDownloadCallback(
context,
onStart = {
statusListener?.onPdfLoadStart()
},
onProgress = { progress, current, total ->
statusListener?.onPdfLoadProgress(progress, current, total)
},
onSuccess = {
try {
initWithFile(it, cacheStrategy)
statusListener?.onPdfLoadSuccess(it.absolutePath)
} catch (e: Exception) {
statusListener?.onError(e)
}
},
onError = {
statusListener?.onError(it)
}
)).start()
}

override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}

Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lifecycle observer is added but the PDF download is started in onCreate. If the lifecycle is already past the CREATED state when this method is called, the onCreate callback will never be triggered and the PDF will never download. Consider checking the current lifecycle state and starting the download immediately if already created, or use a different lifecycle event like onStart.

Suggested change
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
val lifecycleScope = lifecycleOwner.lifecycleScope
this@PdfRendererView.cacheStrategy = cacheStrategy
PdfDownloader(
lifecycleScope,
headers,
url,
cacheStrategy,
PdfDownloadCallback(
context,
onStart = {
statusListener?.onPdfLoadStart()
},
onProgress = { progress, current, total ->
statusListener?.onPdfLoadProgress(progress, current, total)
},
onSuccess = {
try {
initWithFile(it, cacheStrategy)
statusListener?.onPdfLoadSuccess(it.absolutePath)
} catch (e: Exception) {
statusListener?.onError(e)
}
},
onError = {
statusListener?.onError(it)
}
)).start()
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}
val startDownload = {
val lifecycleScope = lifecycleOwner.lifecycleScope
this@PdfRendererView.cacheStrategy = cacheStrategy
PdfDownloader(
lifecycleScope,
headers,
url,
cacheStrategy,
PdfDownloadCallback(
context,
onStart = {
statusListener?.onPdfLoadStart()
},
onProgress = { progress, current, total ->
statusListener?.onPdfLoadProgress(progress, current, total)
},
onSuccess = {
try {
initWithFile(it, cacheStrategy)
statusListener?.onPdfLoadSuccess(it.absolutePath)
} catch (e: Exception) {
statusListener?.onError(e)
}
},
onError = {
statusListener?.onError(it)
}
)).start()
}
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
startDownload()
} else {
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
startDownload()
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}

Copilot uses AI. Check for mistakes.
@afreakyelf
Copy link
Owner

Thanks for the solid refactor! 🙌 The coroutine bridge and lifecycle-aware API are definitely the right direction, and this will make downloads more robust and cancellable.

A couple of things I’d like to see before merging:

  1. Lifecycle integration – right now initWithUrl mixes a Thread.start() with the coroutine flow. Let’s stick to launching in the lifecycle’s coroutineScope and cancel the job in onDestroy. That way everything is cancellable and consistent.
  2. Response closing – make sure the Response is always closed in a finally block to avoid leaks.
  3. Atomic writes – consider writing to a temp file and renaming at the end so canceled/failed downloads don’t leave partial files behind.
  4. OkHttp reuse – if possible, reuse a single OkHttpClient instead of instantiating a new one per downloader.
  5. Tests – it’d be great to add coverage for cancellation, HTTP error propagation, and lifecycle cancel cases.

Other than that, the direction looks great. Once we fix the coroutine wiring and add the cleanup guards, I think this will be ready to go 🚀

@afreakyelf afreakyelf requested a review from Copilot September 27, 2025 18:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Comment on lines +162 to +194

lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
val lifecycleScope = lifecycleOwner.lifecycleScope
this@PdfRendererView.cacheStrategy = cacheStrategy
PdfDownloader(
lifecycleScope,
headers,
url,
cacheStrategy,
PdfDownloadCallback(
context,
onStart = {
statusListener?.onPdfLoadStart()
},
onProgress = { progress, current, total ->
statusListener?.onPdfLoadProgress(progress, current, total)
},
onSuccess = {
try {
initWithFile(it, cacheStrategy)
statusListener?.onPdfLoadSuccess(it.absolutePath)
} catch (e: Exception) {
statusListener?.onError(e)
}
},
onError = {
statusListener?.onError(it)
}
)).start()
}

Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download is started in onCreate, but this may execute multiple times if the lifecycle owner goes through multiple create/destroy cycles. The download should be started immediately when initWithUrl is called, not deferred to onCreate. Consider starting the download directly and using lifecycle observer only for cleanup.

Suggested change
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
val lifecycleScope = lifecycleOwner.lifecycleScope
this@PdfRendererView.cacheStrategy = cacheStrategy
PdfDownloader(
lifecycleScope,
headers,
url,
cacheStrategy,
PdfDownloadCallback(
context,
onStart = {
statusListener?.onPdfLoadStart()
},
onProgress = { progress, current, total ->
statusListener?.onPdfLoadProgress(progress, current, total)
},
onSuccess = {
try {
initWithFile(it, cacheStrategy)
statusListener?.onPdfLoadSuccess(it.absolutePath)
} catch (e: Exception) {
statusListener?.onError(e)
}
},
onError = {
statusListener?.onError(it)
}
)).start()
}
// Start the download immediately
val lifecycleScope = lifecycleOwner.lifecycleScope
this.cacheStrategy = cacheStrategy
PdfDownloader(
lifecycleScope,
headers,
url,
cacheStrategy,
PdfDownloadCallback(
context,
onStart = {
statusListener?.onPdfLoadStart()
},
onProgress = { progress, current, total ->
statusListener?.onPdfLoadProgress(progress, current, total)
},
onSuccess = {
try {
initWithFile(it, cacheStrategy)
statusListener?.onPdfLoadSuccess(it.absolutePath)
} catch (e: Exception) {
statusListener?.onError(e)
}
},
onError = {
statusListener?.onError(it)
}
)
).start()
// Add observer only for cleanup
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +79
suspend fun writeFile(inputStream: InputStream, file: File, totalLength: Long, onProgress: (Long) -> Unit) =
coroutineScope {
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function signature uses an expression body but the implementation spans multiple lines. Consider using a block body for better readability and consistency with the multi-line implementation.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants